/*
 * Copyright (C) 2006-2007 Sun Microsystems, Inc. All rights reserved. Use is
 * subject to license terms.
 */

package org.gwt.beansbinding.observablecollections.client;

import java.util.AbstractList;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * {@code ObservableCollections} provides factory methods for creating
 * observable lists and maps.
 * 
 * @author sky
 * @author geprgopoulos.georgios(at)gmail.com
 */
public final class ObservableCollections {
  /**
   * Creates and returns an {@code ObservableMap} wrapping the supplied
   * {@code Map}.
   * 
   * @param map the {@code Map} to wrap
   * @return an {@code ObservableMap}
   * @throws IllegalArgumentException if {@code map} is {@code null}
   */
  public static <K, V> ObservableMap<K, V> observableMap(Map<K, V> map) {
    if (map == null) {
      throw new IllegalArgumentException("Map must be non-null");
    }
    return new ObservableMapImpl<K, V>(map);
  }

  /**
   * Creates and returns an {@code ObservableList} wrapping the supplied
   * {@code List}.
   * 
   * @param list the {@code List} to wrap
   * @return an {@code ObservableList}
   * @throws IllegalArgumentException if {@code list} is {@code null}
   */
  public static <E> ObservableList<E> observableList(List<E> list) {
    if (list == null) {
      throw new IllegalArgumentException("List must be non-null");
    }
    return new ObservableListImpl<E>(list, false);
  }

  /**
   * Creates and returns an {@code ObservableListHelper} wrapping the supplied
   * {@code List}. If you can track changes to the underlying list, use this
   * method instead of {@code observableList()}.
   * 
   * @param list the {@code List} to wrap
   * @return an {@code ObservableList}
   * @throws IllegalArgumentException if {@code list} is {@code null}
   * 
   * @see #observableList
   */
  public static <E> ObservableListHelper<E> observableListHelper(List<E> list) {
    ObservableListImpl<E> oList = new ObservableListImpl<E>(list, true);
    return new ObservableListHelper<E>(oList);
  }

  /**
   * {@code ObservableListHelper} is created by {@code observableListHelper},
   * and useful when changes to individual elements of the list can be tracked.
   * 
   * @see #observableListHelper
   */
  public static final class ObservableListHelper<E> {
    private final ObservableListImpl<E> list;

    ObservableListHelper(ObservableListImpl<E> list) {
      this.list = list;
    }

    /**
     * Returns the {@code ObservableList}.
     * 
     * @return the observable list
     */
    public ObservableList<E> getObservableList() {
      return list;
    }

    /**
     * Sends notification that the element at the specified index has changed.
     * 
     * @param index the index of the element that has changed
     * @throws ArrayIndexOutOfBoundsException if index is outside the range of
     *           the {@code List} ({@code < 0 || >= size})
     */
    public void fireElementChanged(int index) {
      if (index < 0 || index >= list.size()) {
        throw new ArrayIndexOutOfBoundsException("Illegal index");
      }
      list.fireElementChanged(index);
    }
  }

  private static final class ObservableMapImpl<K, V> extends AbstractMap<K, V>
      implements ObservableMap<K, V> {
    private Map<K, V> map;
    private List<ObservableMapListener> listeners;
    private Set<Map.Entry<K, V>> entrySet;

    ObservableMapImpl(Map<K, V> map) {
      this.map = map;
      // (ggeorg) listeners = new CopyOnWriteArrayList<ObservableMapListener>();
      listeners = new ArrayList<ObservableMapListener>();
    }

    public void clear() {
      // Remove all elements via iterator to trigger notification
      Iterator<K> iterator = keySet().iterator();
      while (iterator.hasNext()) {
        iterator.next();
        iterator.remove();
      }
    }

    public boolean containsKey(Object key) {
      return map.containsKey(key);
    }

    public boolean containsValue(Object value) {
      return map.containsValue(value);
    }

    public Set<Map.Entry<K, V>> entrySet() {
      Set<Map.Entry<K, V>> es = entrySet;
      return es != null ? es : (entrySet = new EntrySet());
    }

    public V get(Object key) {
      return map.get(key);
    }

    public boolean isEmpty() {
      return map.isEmpty();
    }

    public V put(K key, V value) {
      V lastValue;
      if (containsKey(key)) {
        lastValue = map.put(key, value);
        for (ObservableMapListener listener : listeners) {
          listener.mapKeyValueChanged(this, key, lastValue);
        }
      } else {
        lastValue = map.put(key, value);
        for (ObservableMapListener listener : listeners) {
          listener.mapKeyAdded(this, key);
        }
      }
      return lastValue;
    }

    public void putAll(Map<? extends K, ? extends V> m) {
      for (K key : m.keySet()) {
        put(key, m.get(key));
      }
    }

    public V remove(Object key) {
      if (containsKey(key)) {
        V value = map.remove(key);
        for (ObservableMapListener listener : listeners) {
          listener.mapKeyRemoved(this, key, value);
        }
        return value;
      }
      return null;
    }

    public int size() {
      return map.size();
    }

    public void addObservableMapListener(ObservableMapListener listener) {
      listeners.add(listener);
    }

    public void removeObservableMapListener(ObservableMapListener listener) {
      listeners.remove(listener);
    }

    private class EntryIterator implements Iterator<Map.Entry<K, V>> {
      private Iterator<Map.Entry<K, V>> realIterator;
      private Map.Entry<K, V> last;

      EntryIterator() {
        realIterator = map.entrySet().iterator();
      }

      public boolean hasNext() {
        return realIterator.hasNext();
      }

      public Map.Entry<K, V> next() {
        last = realIterator.next();
        return last;
      }

      public void remove() {
        if (last == null) {
          throw new IllegalStateException();
        }
        Object toRemove = last.getKey();
        last = null;
        ObservableMapImpl.this.remove(toRemove);
      }
    }

    private class EntrySet extends AbstractSet<Map.Entry<K, V>> {
      public Iterator<Map.Entry<K, V>> iterator() {
        return new EntryIterator();
      }

      @SuppressWarnings("unchecked")
      public boolean contains(Object o) {
        if (!(o instanceof Map.Entry)) {
          return false;
        }
        Map.Entry<K, V> e = (Map.Entry<K, V>) o;
        return containsKey(e.getKey());
      }

      @SuppressWarnings("unchecked")
      public boolean remove(Object o) {
        if (o instanceof Map.Entry) {
          K key = ((Map.Entry<K, V>) o).getKey();
          if (containsKey(key)) {
            remove(key);
            return true;
          }
        }
        return false;
      }

      public int size() {
        return ObservableMapImpl.this.size();
      }

      public void clear() {
        ObservableMapImpl.this.clear();
      }
    }
  }

  private static final class ObservableListImpl<E> extends AbstractList<E>
      implements ObservableList<E> {
    private final boolean supportsElementPropertyChanged;
    private List<E> list;
    private List<ObservableListListener> listeners;

    ObservableListImpl(List<E> list, boolean supportsElementPropertyChanged) {
      this.list = list;
      // (ggeorg) listeners = new
      // CopyOnWriteArrayList<ObservableListListener>();
      listeners = new ArrayList<ObservableListListener>();
      this.supportsElementPropertyChanged = supportsElementPropertyChanged;
    }

    public E get(int index) {
      return list.get(index);
    }

    public int size() {
      return list.size();
    }

    public E set(int index, E element) {
      E oldValue = list.set(index, element);
      for (ObservableListListener listener : listeners) {
        listener.listElementReplaced(this, index, oldValue);
      }
      return oldValue;
    }

    public void add(int index, E element) {
      list.add(index, element);
      // (ggeorg) modCount++;
      for (ObservableListListener listener : listeners) {
        listener.listElementsAdded(this, index, 1);
      }
    }

    public E remove(int index) {
      E oldValue = list.remove(index);
      // (ggeorg) modCount++;
      for (ObservableListListener listener : listeners) {
        listener.listElementsRemoved(this, index,
            java.util.Collections.singletonList(oldValue));
      }
      return oldValue;
    }

    public boolean addAll(Collection<? extends E> c) {
      return addAll(size(), c);
    }

    public boolean addAll(int index, Collection<? extends E> c) {
      if (list.addAll(index, c)) {
        // (ggeorg) modCount++;
        for (ObservableListListener listener : listeners) {
          listener.listElementsAdded(this, index, c.size());
        }
      }
      return false;
    }

    public void clear() {
      List<E> dup = new ArrayList<E>(list);
      list.clear();
      // (ggeorg) modCount++;
      if (dup.size() != 0) {
        for (ObservableListListener listener : listeners) {
          listener.listElementsRemoved(this, 0, dup);
        }
      }
    }

    public boolean containsAll(Collection<?> c) {
      return list.containsAll(c);
    }

    public <T> T[] toArray(T[] a) {
      return list.toArray(a);
    }

    public Object[] toArray() {
      return list.toArray();
    }

    private void fireElementChanged(int index) {
      for (ObservableListListener listener : listeners) {
        listener.listElementPropertyChanged(this, index);
      }
    }

    public void addObservableListListener(ObservableListListener listener) {
      listeners.add(listener);
    }

    public void removeObservableListListener(ObservableListListener listener) {
      listeners.remove(listener);
    }

    public boolean supportsElementPropertyChanged() {
      return supportsElementPropertyChanged;
    }
  }
}
